/********************************************************************
 *	Kalendae, a framework agnostic javascript date picker           *
 *	Copyright(c) 2012 Jarvis Badgley (chipersoft@gmail.com)         *
 *	http://github.com/ChiperSoft/Kalendae                           *
 *	Version 0.2                                                     *
 ********************************************************************/

(function (undefined) {

    var today;

    var Kalendae = function (targetElement, options) {
        //if the first argument isn't an element and isn't a string, assume that it is the options object
        if (!(targetElement instanceof Element || typeof targetElement === 'string')) options = targetElement;

        var self = this,
            classes = self.classes,
            opts = self.settings = util.merge(self.defaults, {attachTo:targetElement}, options || {}),
            $container = self.container = util.make('div', {'class':classes.container}),
            calendars = self.calendars = [],
            startDay = moment().day(opts.weekStart),
            vsd,
            columnHeaders = [],
            $cal,
            $title,
            $caption,
            $header,
            $days, dayNodes = [],
            $span,
            i = 0,
            j = opts.months;

        if (util.isIE8()) util.addClassName($container, 'ie8');

        //generate the column headers (Su, Mo, Tu, etc)
        i = 7;
        while (i--) {
            columnHeaders.push( startDay.format('ddd').substr(0,opts.columnHeaderLength) );
            startDay.add('days',1);
        }

        //setup publish/subscribe and apply any subscriptions passed in settings
        MinPubSub(self);
        if (typeof opts.subscribe === 'object') {
            for (i in opts.subscribe) if (opts.subscribe.hasOwnProperty(i)) {
                self.subscribe(i, opts.subscribe[i]);
            }
        }

        //process default selected dates
        self._sel = [];
        if (!!opts.selected) self.setSelected(opts.selected, false);

        //set the view month
        if (!!opts.viewStartDate) {
            vsd = moment(opts.viewStartDate, opts.format);
        } else if (self._sel.length > 0) {
            vsd = moment(self._sel[0]);
        } else {
            vsd = moment();
        }
        self.viewStartDate = vsd.date(1);

        var viewDelta = ({
            'past'			: opts.months-1,
            'today-past'	: opts.months-1,
            'any'			: opts.months>2?Math.floor(opts.months/2):0,
            'today-future'	: 0,
            'future'		: 0
        })[this.settings.direction];


        if (viewDelta && moment().month()==moment(self.viewStartDate).month()){
            self.viewStartDate = moment(self.viewStartDate).subtract({M:viewDelta}).date(1);
        }


        if (typeof opts.blackout === 'function') {
            self.blackout = opts.blackout;
        } else if (!!opts.blackout) {
            var bdates = parseDates(opts.blackout, opts.parseSplitDelimiter);
            self.blackout = function (input) {
                input = moment(input).yearDay();
                if (input < 1 || !self._sel || self._sel.length < 1) return false;
                var i = bdates.length;
                while (i--) if (bdates[i].yearDay() === input) return true;
                return false;
            }
        } else {
            self.blackout = function () {return false;}
        }


        self.direction = self.directions[opts.direction] ? self.directions[opts.direction] : self.directions['any'];


        //for the total months setting, generate N calendar views and add them to the container
        j = Math.max(opts.months,1);
        while (j--) {
            $cal = util.make('div', {'class':classes.calendar}, $container);

            $cal.setAttribute('data-cal-index', j);
            if (opts.months > 1) {
                if (j == Math.max(opts.months-1,1)) util.addClassName($cal, classes.monthFirst);
                else if (j === 0) util.addClassName($cal, classes.monthLast);
                else util.addClassName($cal, classes.monthMiddle);
            }

            //title bar
            $title = util.make('div', {'class':classes.title}, $cal);
            util.make('a', {'class':classes.previousYear}, $title);	//previous button
            util.make('a', {'class':classes.previousMonth}, $title);	//previous button
            util.make('a', {'class':classes.nextYear}, $title);		//next button
            util.make('a', {'class':classes.nextMonth}, $title);		//next button
            $caption = util.make('span', {'class':classes.caption}, $title);	//title caption

            //column headers
            $header = util.make('div', {'class':classes.header}, $cal);
            i = 0;
            do {
                $span = util.make('span', {}, $header);
                $span.innerHTML = columnHeaders[i];
            } while (++i < 7)

            //individual day cells
            $days = util.make('div', {'class':classes.days}, $cal);
            i = 0;
            dayNodes = [];
            while (i++ < 42) {
                dayNodes.push(util.make('span', {}, $days));
            }

            //store each calendar view for easy redrawing
            calendars.push({
                caption:$caption,
                days:dayNodes
            });

            if (j) util.make('div', {'class':classes.monthSeparator}, $container);
        }

        self.draw();

        util.addEvent($container, 'mousedown', function (event, target) {
            var clickedDate;
            if (util.hasClassName(target, classes.nextMonth)) {
                //NEXT MONTH BUTTON
                if (!self.disableNext && self.publish('view-changed', self, ['next-month']) !== false) {
                    self.viewStartDate.add('months',1);
                    self.draw();
                }
                return false;

            } else if (util.hasClassName(target, classes.previousMonth)) {
                //PREVIOUS MONTH BUTTON
                if (!self.disablePreviousMonth && self.publish('view-changed', self, ['previous-month']) !== false) {
                    self.viewStartDate.subtract('months',1);
                    self.draw();
                }
                return false;

            } else if (util.hasClassName(target, classes.nextYear)) {
                //NEXT MONTH BUTTON
                if (!self.disableNext && self.publish('view-changed', self, ['next-year']) !== false) {
                    self.viewStartDate.add('years',1);
                    self.draw();
                }
                return false;

            } else if (util.hasClassName(target, classes.previousYear)) {
                //PREVIOUS MONTH BUTTON
                if (!self.disablePreviousMonth && self.publish('view-changed', self, ['previous-year']) !== false) {
                    self.viewStartDate.subtract('years',1);
                    self.draw();
                }
                return false;



            } else if (util.hasClassName(target.parentNode, classes.days) && util.hasClassName(target, classes.dayActive) && (clickedDate = target.getAttribute('data-date'))) {
                //DAY CLICK
                clickedDate = moment(clickedDate, opts.dayAttributeFormat).hours(12);
                if (self.publish('date-clicked', self, [clickedDate]) !== false) {

                    switch (opts.mode) {
                        case 'multiple':
                            if (!self.addSelected(clickedDate)) self.removeSelected(clickedDate);
                            break;
                        case 'range':
                            self.addSelected(clickedDate);
                            break;
                        case 'single':
                        /* falls through */
                        default:
                            self.addSelected(clickedDate);
                            break;
                    }

                }
                return false;

            }
            return false;
        });


        if (!!(opts.attachTo = util.$(opts.attachTo))) {
            opts.attachTo.appendChild($container);
        }

    };

    Kalendae.prototype = {
        defaults : {
            attachTo:				null,			/* the element to attach the root container to. can be string or DOMElement */
            months:					1,				/* total number of months to display side by side */
            weekStart:				1,				/* day to use for the start of the week. 0 is Sunday */
            direction:				'any',			/* past, today-past, any, today-future, future */
            directionScrolling:		true,			/* if a direction other than any is defined, prevent scrolling out of range */
            viewStartDate:			null,			/* date in the month to display.  When multiple months, this is the left most */
            blackout:				null,			/* array of dates, or function to be passed a date */
            selected:				null,			/* dates already selected.  can be string, date, or array of strings or dates. */
            mode:					'single',		/* single, multiple, range */
            format:					null,			/* string used for parsing dates. */
            subscribe:				null,			/* object containing events to subscribe to */

            columnHeaderLength:		2,				/* number of characters to show in the column headers */
            titleFormat:			'MMMM, YYYY',	/* format mask for month titles. See momentjs.com for rules */
            dayNumberFormat:		'D',			/* format mask for individual days */
            dayAttributeFormat:		'YYYY-MM-DD',	/* format mask for the data-date attribute set on every span */
            parseSplitDelimiter:	/,\s*|\s+-\s+/,	/* regex to use for splitting multiple dates from a passed string */
            rangeDelimiter:			' - ',			/* string to use between dates when outputting in range mode */
            multipleDelimiter:		', ',			/* string to use between dates when outputting in multiple mode */

            dateClassMap:			{}
        },
        classes : {
            container		:'kalendae',
            calendar		:'k-calendar',
            monthFirst		:'k-first-month',
            monthMiddle		:'k-middle-month',
            monthLast		:'k-last-month',
            title			:'k-title',
            previousMonth	:'k-btn-previous-month',
            nextMonth		:'k-btn-next-month',
            previousYear	:'k-btn-previous-year',
            nextYear		:'k-btn-next-year',
            caption			:'k-caption',
            header			:'k-header',
            days			:'k-days',
            dayOutOfMonth	:'k-out-of-month',
            dayActive		:'k-active',
            daySelected		:'k-selected',
            dayInRange		:'k-range',
            dayToday		:'k-today',
            monthSeparator	:'k-separator',
            disablePreviousMonth	:'k-disable-previous-month-btn',
            disableNextMonth		:'k-disable-next-month-btn',
            disablePreviousYear		:'k-disable-previous-year-btn',
            disableNextYear			:'k-disable-next-year-btn'
        },

        disablePreviousMonth: false,
        disableNextMonth: false,
        disablePreviousYear: false,
        disableNextYear: false,

        directions: {
            'past'			:function (date) {return moment(date).yearDay() >= today.yearDay();},
            'today-past'	:function (date) {return moment(date).yearDay() > today.yearDay();},
            'any'			:function (date) {return false;},
            'today-future'	:function (date) {return moment(date).yearDay() < today.yearDay();},
            'future'		:function (date) {return moment(date).yearDay() <= today.yearDay();}
        },

        getSelectedAsDates : function () {
            var out = [];
            var i=0, c = this._sel.length;
            for (;i<c;i++) {
                out.push(this._sel[i].toDate());
            }
            return out;
        },

        getSelectedAsText : function (format) {
            var out = [];
            var i=0, c = this._sel.length;
            for (;i<c;i++) {
                out.push(this._sel[i].format(format || this.settings.format || 'YYYY-MM-DD'))
            }
            return out;
        },

        getSelectedRaw : function () {
            var out = [];
            var i=0, c = this._sel.length;
            for (;i<c;i++) {
                out.push(moment(this._sel[i]))
            }
            return out;
        },

        getSelected : function (format) {
            var sel = this.getSelectedAsText(format);
            switch (this.settings.mode) {
                case 'range':
                    sel.splice(2); //shouldn't be more than two, but lets just make sure.
                    return sel.join(this.settings.rangeDelimiter);

                case 'multiple':
                    return sel.join(this.settings.multipleDelimiter);

                case 'single':
                /* falls through */
                default:
                    return sel[0];
            }
        },

        isSelected : function (input) {
            input = moment(input).yearDay();
            if (input < 1 || !this._sel || this._sel.length < 1) return false;

            switch (this.settings.mode) {
                case 'range':
                    var a = this._sel[0] ? this._sel[0].yearDay() : 0,
                        b = this._sel[1] ? this._sel[1].yearDay() : 0;

                    if (a === input || b === input) return 1;
                    if (!a || !b) return 0;

                    if ((input > a && input < b) || (a<b && input < a && input > b))  return -1;
                    return false;

                case 'multiple':
                    var i = this._sel.length;
                    while (i--) {
                        if (this._sel[i].yearDay() === input) {
                            return true;
                        }
                    }
                    return false;


                case 'single':
                /* falls through */
                default:
                    return (this._sel[0] && (this._sel[0].yearDay() === input));
            }

            return false;
        },

        setSelected : function (input, draw) {
            this._sel = parseDates(input, this.settings.parseSplitDelimiter, this.settings.format);
            this._sel.sort(function (a,b) {return a.yearDay() - b.yearDay();});

            if (draw !== false) this.draw();
        },

        addSelected : function (date, draw) {
            date = moment(date).hours(12);
            switch (this.settings.mode) {
                case 'multiple':
                    if (!this.isSelected(date)) this._sel.push(date);
                    else return false;
                    break;
                case 'range':

                    if (this._sel.length !== 1) this._sel = [date];
                    else {
                        if (date.yearDay() > this._sel[0].yearDay()) this._sel[1] = date;
                        else this._sel = [date, this._sel[0]];
                    }
                    break;
                case 'single':
                /* falls through */
                default:
                    this._sel = [date];
                    break;
            }
            this._sel.sort(function (a,b) {return a.yearDay() - b.yearDay();});
            this.publish('change', this);
            if (draw !== false) this.draw();
            return true;
        },

        removeSelected : function (date, draw) {
            date = moment(date).yearDay();
            var i = this._sel.length;
            while (i--) {
                if (this._sel[i].yearDay() === date) {
                    this._sel.splice(i,1);
                    this.publish('change', this);
                    if (draw !== false) this.draw();
                    return true;
                }
            }
            return false;
        },

        draw : function draw() {
            // return;
            var month = moment(this.viewStartDate).hours(12), //force middle of the day to avoid any weird date shifts
                day,
                classes = this.classes,
                cal,
                $span,
                klass,
                i=0, c,
                j=0, k,
                s,
                dateString,
                opts = this.settings;

            c = this.calendars.length;

            do {
                day = moment(month).date(1);
                day.day( day.day() < this.settings.weekStart ? this.settings.weekStart-7 : this.settings.weekStart);
                //if the first day of the month is less than our week start, back up a week

                cal = this.calendars[i];
                cal.caption.innerHTML = month.format(this.settings.titleFormat);
                j = 0;
                do {
                    $span = cal.days[j];

                    klass = [];

                    s = this.isSelected(day);

                    if (s) klass.push(({'-1':classes.dayInRange,'1':classes.daySelected, 'true':classes.daySelected})[s]);

                    if (day.month() != month.month()) klass.push(classes.dayOutOfMonth);
                    else if (!(this.blackout(day) || this.direction(day)) || s>0) klass.push(classes.dayActive);

                    if (day.yearDay() === today.yearDay()) klass.push(classes.dayToday);

                    dateString = day.format(this.settings.dayAttributeFormat);
                    if (opts.dateClassMap[dateString]) klass.push(opts.dateClassMap[dateString]);

                    $span.innerHTML = day.format(opts.dayNumberFormat);
                    $span.className = klass.join(' ');
                    $span.setAttribute('data-date', dateString);


                    day.add('days',1);
                } while (++j < 42);
                month.add('months',1);
            } while (++i < c);

            if (opts.directionScrolling) {
                var diff = -(moment().diff(month, 'months'));
                if (opts.direction==='today-past' || opts.direction==='past') {

                    if (diff <= 0) {
                        this.disableNextMonth = false;
                        util.removeClassName(this.container, classes.disableNextMonth);
                    } else {
                        this.disableNextMonth = true;
                        util.addClassName(this.container, classes.disableNextMonth);
                    }

                } else if (opts.direction==='today-future' || opts.direction==='future') {

                    if (diff > opts.months) {
                        this.disablePreviousMonth = false;
                        util.removeClassName(this.container, classes.disablePreviousMonth);
                    } else {
                        this.disablePreviousMonth = true;
                        util.addClassName(this.container, classes.disablePreviousMonth);
                    }

                }


                if (opts.direction==='today-past' || opts.direction==='past') {
                    if (month.add({Y:1}).diff(moment(), 'years') < 0) {
                        this.disableNextYear = false;
                        util.removeClassName(this.container, classes.disableNextYear);
                    } else {
                        this.disableNextYear = true;
                        util.addClassName(this.container, classes.disableNextYear);
                    }

                } else if (opts.direction==='today-future' || opts.direction==='future') {
                    if (month.subtract({Y:1}).diff(moment(), 'years') > 0) {
                        this.disablePreviousYear = false;
                        util.removeClassName(this.container, classes.disablePreviousYear);
                    } else {
                        this.disablePreviousYear = true;
                        util.addClassName(this.container, classes.disablePreviousYear);
                    }

                }

            }
        }
    }

    var parseDates = function (input, delimiter, format) {
        var output = [];

        if (typeof input === 'string') {
            input = input.split(delimiter);
        } else if (!util.isArray(input)) {
            input = [input];
        }

        var c = input.length;
        i = 0;
        do {
            if (input[i]) output.push( moment(input[i], format).hours(12) );
        } while (++i < c);

        return output;
    }



    window.Kalendae = Kalendae;

    var util = Kalendae.util = {

        isIE8: function() {
            return !!( (/msie 8./i).test(navigator.appVersion) && !(/opera/i).test(navigator.userAgent) && window.ActiveXObject && XDomainRequest && !window.msPerformance );
        },

// ELEMENT FUNCTIONS

        $: function (elem) {
            return (typeof elem == 'string') ? document.getElementById(elem) : elem;
        },

        $$: function (selector) {
            return document.querySelectorAll(selector);
        },

        make: function (tagName, attributes, attach) {
            var k, e = document.createElement(tagName);
            if (!!attributes) for (k in attributes) if (attributes.hasOwnProperty(k)) e.setAttribute(k, attributes[k]);
            if (!!attach) attach.appendChild(e);
            return e;
        },

        // Returns true if the DOM element is visible, false if it's hidden.
        // Checks if display is anything other than none.
        isVisible: function (elem) {
            // shamelessly copied from jQuery
            return elem.offsetWidth > 0 || elem.offsetHeight > 0;
        },

        getStyle: function (elem, styleProp) {
            var y;
            if (elem.currentStyle) {
                y = elem.currentStyle[styleProp];
            } else if (window.getComputedStyle) {
                y = window.getComputedStyle(elem, null)[styleProp];
            }
            return y;
        },

        domReady:function (f){/in/.test(document.readyState) ? setTimeout(function() {util.domReady(f);},9) : f()},

        // Adds a listener callback to a DOM element which is fired on a specified
        // event.  Callback is sent the event object and the element that triggered the event
        addEvent: function (elem, eventName, callback) {
            var listener = function (event) {
                event = event || window.event;
                var target = event.target || event.srcElement;
                var block = callback.apply(elem, [event, target]);
                if (block === false) {
                    if (!!event.preventDefault) event.preventDefault();
                    else {
                        event.returnValue = false;
                        event.cancelBubble = true;
                    }
                }
                return block;
            };
            if (elem.attachEvent) { // IE only.  The "on" is mandatory.
                elem.attachEvent("on" + eventName, listener);
            } else { // Other browsers.
                elem.addEventListener(eventName, listener, false);
            }
            return listener;
        },

        // Removes a listener callback from a DOM element which is fired on a specified
        // event.
        removeEvent: function (elem, event, listener) {
            if (elem.detachEvent) {	// IE only.  The "on" is mandatory.
                elem.detachEvent("on" + event, listener);
            } else { // Other browsers.
                elem.removeEventListener(event, listener, false);
            }
        },

        hasClassName: function(elem, className) { //copied and modified from Prototype.js
            if (!(elem = util.$(elem))) return false;
            var eClassName = elem.className;
            return (eClassName.length > 0 && (eClassName == className || new RegExp("(^|\\s)" + className + "(\\s|$)").test(eClassName)));
        },

        addClassName: function(elem, className) { //copied and modified from Prototype.js
            if (!(elem = util.$(elem))) return;
            if (!util.hasClassName(elem, className)) elem.className += (elem.className ? ' ' : '') + className;
        },

        removeClassName: function(elem, className) { //copied and modified from Prototype.js
            if (!(elem = util.$(elem))) return;
            elem.className = util.trimString(elem.className.replace(new RegExp("(^|\\s+)" + className + "(\\s+|$)"), ' '));
        },

        isFixed: function (elem) {
            do {
                if (util.getStyle(elem, 'position') === 'fixed') return true;
            } while ((elem = elem.offsetParent));
            return false;
        },

        getPosition: function (elem, isInner) {
            var x = elem.offsetLeft,
                y = elem.offsetTop,
                r = {};

            if (!isInner) {
                while ((elem = elem.offsetParent)) {
                    x += elem.offsetLeft;
                    y += elem.offsetTop;
                }
            }

            r[0] = r.left = x;
            r[1] = r.top = y;
            return r;
        },

        getHeight: function (elem) {
            return elem.offsetHeight || elem.scrollHeight;
        },

        getWidth: function (elem) {
            return elem.offsetWidth || elem.scrollWidth;
        },


// TEXT FUNCTIONS

        trimString: function (input) {
            return input.replace(/^\s+/, '').replace(/\s+$/, '');
        },


// OBJECT FUNCTIONS

        merge: function () {
            /* Combines multiple objects into one.
             * Syntax: util.extend([true], object1, object2, ... objectN)
             * If first argument is true, function will merge recursively.
             */

            var deep = (arguments[0]===true),
                d = {},
                i = deep?1:0;

            var _c = function (a, b) {
                if (typeof b !== 'object') return;
                for (var k in b) if (b.hasOwnProperty(k)) {
                    //if property is an object or array, merge the contents instead of overwriting, if extend() was called as such
                    if (deep && typeof a[k] === 'object' && typeof b[k] === 'object') _update(a[k], b[k]);
                    else a[k] = b[k];
                }
                return a;
            }

            for (; i < arguments.length; i++) {
                _c(d, arguments[i]);
            }
            return d;
        },

        isArray: function (array) {
            return !(
                !array ||
                (!array.length || array.length === 0) ||
                typeof array !== 'object' ||
                !array.constructor ||
                array.nodeType ||
                array.item
                );
        }
    };


//auto-initializaiton code
    Kalendae.util.domReady(function () {
        var els = util.$$('.auto-kal'),
            i = els.length,
            e;

        while (i--) {
            e = els[i];
            if (e.tagName === 'INPUT') {
                //if element is an input, bind a popup calendar to the input.
                new Kalendae.Input(e);
            } else {
                //otherwise, insert a flat calendar into the element.
                new Kalendae({attachTo:e});
            }

        }
    });

    Kalendae.Input = function (targetElement, options) {
        var $input = this.input = util.$(targetElement),
            overwriteInput;

        if (!$input || $input.tagName !== 'INPUT') throw "First argument for Kalendae.Input must be an <input> element or a valid element id.";

        var self = this,
            classes = self.classes
        opts = self.settings = util.merge(self.defaults, options);

        //force attachment to the body
        opts.attachTo = window.document.body;

        //if no override provided, use the input's contents
        if (!opts.selected) opts.selected = $input.value;
        else overwriteInput = true;

        //call our parent constructor
        Kalendae.call(self, opts);

        //create the close button
        if (opts.closeButton) {
            var $closeButton = util.make('a', {'class':classes.closeButton}, self.container)
            util.addEvent($closeButton, 'click', function () {
                $input.blur();
            });
        }

        if (overwriteInput) $input.value = self.getSelected();

        var $container = self.container,
            noclose = false;

        $container.style.display = 'none';
        util.addClassName($container, classes.positioned);

        util.addEvent($container, 'mousedown', function (event, target) {
            noclose = true; //IE8 doesn't obey event blocking when it comes to focusing, so we have to do this shit.
        });
        util.addEvent(window.document, 'mousedown', function (event, target) {
            noclose = false;
        });

        util.addEvent($input, 'focus', function () {
            self.setSelected(this.value);
            self.show();
        });

        util.addEvent($input, 'blur', function () {
            if (noclose) {
                noclose = false;
                $input.focus();
            }
            else self.hide();
        });
        util.addEvent($input, 'keyup', function (event) {
            self.setSelected(this.value);
        });

        self.subscribe('change', function () {
            $input.value = self.getSelected();
        });

    };

    Kalendae.Input.prototype = util.merge(Kalendae.prototype, {
        defaults : util.merge(Kalendae.prototype.defaults, {
            format: 'YYYY/MM/DD',
            side: 'bottom',
            closeButton: true,
            offsetLeft: 0,
            offsetTop: 0
        }),
        classes : util.merge(Kalendae.prototype.classes, {
            positioned : 'k-floating',
            closeButton: 'k-btn-close'
        }),

        show : function () {
            var $container = this.container,
                style = $container.style,
                $input = this.input,
                pos = util.getPosition($input);

            style.display = '';
            switch (opts.side) {
                case 'left':
                    style.left = (pos.left - util.getWidth($container) + this.settings.offsetLeft) + 'px';
                    style.top  = (pos.top + this.settings.offsetTop) + 'px';
                    break;
                case 'right':
                    style.left = (pos.left + util.getWidth($input)) + 'px';
                    style.top  = (pos.top + this.settings.offsetTop) + 'px';
                    break;
                case 'top':
                    style.left = (pos.left + this.settings.offsetLeft) + 'px';
                    style.top  = (pos.top - util.getHeight($container) + this.settings.offsetTop) + 'px';
                    break;
                case 'bottom':
                /* falls through */
                default:
                    style.left = (pos.left + this.settings.offsetLeft) + 'px';
                    style.top  = (pos.top + util.getHeight($input) + this.settings.offsetTop) + 'px';
                    break;
            }

            style.position = util.isFixed($input) ? 'fixed' : 'absolute';

        },

        hide : function () {
            this.container.style.display = 'none';
        }

    });


    /*!
     * MinPubSub, modified for use on Kalendae
     * Copyright(c) 2011 Daniel Lamb <daniellmb.com>
     * https://github.com/daniellmb/MinPubSub
     * MIT Licensed
     */

    var MinPubSub = function(d){

        if (!d) d = this;

        // the topic/subscription hash
        var cache = d.c_ || {}; //check for "c_" cache for unit testing

        d.publish = function(/* String */ topic, /* Object */ target, /* Array? */ args){
            // summary:
            //		Publish some data on a named topic.
            // topic: String
            //		The channel to publish on
            // args: Array?
            //		The data to publish. Each array item is converted into an ordered
            //		arguments on the subscribed functions.
            //
            // example:
            //		Publish stuff on '/some/topic'. Anything subscribed will be called
            //		with a function signature like: function(a,b,c){ ... }
            //
            //		publish("/some/topic", ["a","b","c"]);

            var subs = cache[topic],
                len = subs ? subs.length : 0,
                r;

            //can change loop or reverse array if the order matters
            while(len--){
                r = subs[len].apply(target, args || []);
                if (typeof r === 'boolean') return r;
            }
        };

        d.subscribe = function(/* String */ topic, /* Function */ callback, /* Boolean */ topPriority){
            // summary:
            //		Register a callback on a named topic.
            // topic: String
            //		The channel to subscribe to
            // callback: Function
            //		The handler event. Anytime something is publish'ed on a
            //		subscribed channel, the callback will be called with the
            //		published array as ordered arguments.
            //
            // returns: Array
            //		A handle which can be used to unsubscribe this particular subscription.
            //
            // example:
            //		subscribe("/some/topic", function(a, b, c){ /* handle data */ });

            if(!cache[topic]){
                cache[topic] = [];
            }
            if (topPriority)
                cache[topic].push(callback);
            else
                cache[topic].unshift(callback);
            return [topic, callback]; // Array
        };

        d.unsubscribe = function(/* Array */ handle){
            // summary:
            //		Disconnect a subscribed function for a topic.
            // handle: Array
            //		The return value from a subscribe call.
            // example:
            //		var handle = subscribe("/some/topic", function(){});
            //		unsubscribe(handle);

            var subs = cache[handle[0]],
                callback = handle[1],
                len = subs ? subs.length : 0;

            while(len--){
                if(subs[len] === callback){
                    subs.splice(len, 1);
                }
            }
        };

    };// moment.js
// Altered slightly for use in Kalendae.js
// version : 1.5.0
// author : Tim Wood
// license : MIT
// momentjs.com

    var moment = Kalendae.moment = (function (Date, undefined) {

        var moment,
            round = Math.round,
            languages = {},
            hasModule = (typeof module !== 'undefined'),
            paramsToParse = 'months|monthsShort|monthsParse|weekdays|weekdaysShort|longDateFormat|calendar|relativeTime|ordinal|meridiem'.split('|'),
            i,
            jsonRegex = /^\/?Date\((\-?\d+)/i,
            charactersToReplace = /(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|dddd?|do?|w[o|w]?|YYYY|YY|a|A|hh?|HH?|mm?|ss?|zz?|ZZ?|LT|LL?L?L?)/g,
            nonuppercaseLetters = /[^A-Z]/g,
            timezoneRegex = /\([A-Za-z ]+\)|:[0-9]{2} [A-Z]{3} /g,
            tokenCharacters = /(\\)?(MM?M?M?|dd?d?d|DD?D?D?|YYYY|YY|a|A|hh?|HH?|mm?|ss?|ZZ?|T)/g,
            inputCharacters = /(\\)?([0-9]+|([a-zA-Z\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+|([\+\-]\d\d:?\d\d))/gi,
            isoRegex = /\d{4}.\d\d.\d\d(T(\d\d(.\d\d(.\d\d)?)?)?([\+\-]\d\d:?\d\d)?)?/,
            isoFormat = 'YYYY-MM-DDTHH:mm:ssZ',
            isoTimes = [
                ['HH:mm:ss', /T\d\d:\d\d:\d\d/],
                ['HH:mm', /T\d\d:\d\d/],
                ['HH', /T\d\d/]
            ],
            timezoneParseRegex = /([\+\-]|\d\d)/gi,
            VERSION = "1.5.0",
            shortcuts = 'Month|Date|Hours|Minutes|Seconds|Milliseconds'.split('|');

        // Moment prototype object
        function Moment(date, isUTC) {
            this._d = date;
            this._isUTC = !!isUTC;
        }

        // left zero fill a number
        // see http://jsperf.com/left-zero-filling for performance comparison
        function leftZeroFill(number, targetLength) {
            var output = number + '';
            while (output.length < targetLength) {
                output = '0' + output;
            }
            return output;
        }

        // helper function for _.addTime and _.subtractTime
        function dateAddRemove(date, _input, adding, val) {
            var isString = (typeof _input === 'string'),
                input = isString ? {} : _input,
                ms, d, M, currentDate;
            if (isString && val) {
                input[_input] = +val;
            }
            ms = (input.ms || input.milliseconds || 0) +
                (input.s || input.seconds || 0) * 1e3 + // 1000
                (input.m || input.minutes || 0) * 6e4 + // 1000 * 60
                (input.h || input.hours || 0) * 36e5; // 1000 * 60 * 60
            d = (input.d || input.days || 0) +
                (input.w || input.weeks || 0) * 7;
            M = (input.M || input.months || 0) +
                (input.y || input.years || 0) * 12;
            if (ms) {
                date.setTime(+date + ms * adding);
            }
            if (d) {
                date.setDate(date.getDate() + d * adding);
            }
            if (M) {
                currentDate = date.getDate();
                date.setDate(1);
                date.setMonth(date.getMonth() + M * adding);
                date.setDate(Math.min(new Date(date.getFullYear(), date.getMonth() + 1, 0).getDate(), currentDate));
            }
            return date;
        }

        // check if is an array
        function isArray(input) {
            return Object.prototype.toString.call(input) === '[object Array]';
        }

        // convert an array to a date.
        // the array should mirror the parameters below
        // note: all values past the year are optional and will default to the lowest possible value.
        // [year, month, day , hour, minute, second, millisecond]
        function dateFromArray(input) {
            return new Date(input[0], input[1] || 0, input[2] || 1, input[3] || 0, input[4] || 0, input[5] || 0, input[6] || 0);
        }

        // format date using native date object
        function formatMoment(m, inputString) {
            var currentMonth = m.month(),
                currentDate = m.date(),
                currentYear = m.year(),
                currentDay = m.day(),
                currentHours = m.hours(),
                currentMinutes = m.minutes(),
                currentSeconds = m.seconds(),
                currentZone = -m.zone(),
                ordinal = moment.ordinal,
                meridiem = moment.meridiem;
            // check if the character is a format
            // return formatted string or non string.
            //
            // uses switch/case instead of an object of named functions (like http://phpjs.org/functions/date:380)
            // for minification and performance
            // see http://jsperf.com/object-of-functions-vs-switch for performance comparison
            function replaceFunction(input) {
                // create a couple variables to be used later inside one of the cases.
                var a, b;
                switch (input) {
                    // MONTH
                    case 'M' :
                        return currentMonth + 1;
                    case 'Mo' :
                        return (currentMonth + 1) + ordinal(currentMonth + 1);
                    case 'MM' :
                        return leftZeroFill(currentMonth + 1, 2);
                    case 'MMM' :
                        return moment.monthsShort[currentMonth];
                    case 'MMMM' :
                        return moment.months[currentMonth];
                    // DAY OF MONTH
                    case 'D' :
                        return currentDate;
                    case 'Do' :
                        return currentDate + ordinal(currentDate);
                    case 'DD' :
                        return leftZeroFill(currentDate, 2);
                    // DAY OF YEAR
                    case 'DDD' :
                        a = new Date(currentYear, currentMonth, currentDate);
                        b = new Date(currentYear, 0, 1);
                        return ~~ (((a - b) / 864e5) + 1.5);
                    case 'DDDo' :
                        a = replaceFunction('DDD');
                        return a + ordinal(a);
                    case 'DDDD' :
                        return leftZeroFill(replaceFunction('DDD'), 3);
                    // WEEKDAY
                    case 'd' :
                        return currentDay;
                    case 'do' :
                        return currentDay + ordinal(currentDay);
                    case 'ddd' :
                        return moment.weekdaysShort[currentDay];
                    case 'dddd' :
                        return moment.weekdays[currentDay];
                    // WEEK OF YEAR
                    case 'w' :
                        a = new Date(currentYear, currentMonth, currentDate - currentDay + 5);
                        b = new Date(a.getFullYear(), 0, 4);
                        return ~~ ((a - b) / 864e5 / 7 + 1.5);
                    case 'wo' :
                        a = replaceFunction('w');
                        return a + ordinal(a);
                    case 'ww' :
                        return leftZeroFill(replaceFunction('w'), 2);
                    // YEAR
                    case 'YY' :
                        return leftZeroFill(currentYear % 100, 2);
                    case 'YYYY' :
                        return currentYear;
                    // AM / PM
                    case 'a' :
                        return currentHours > 11 ? meridiem.pm : meridiem.am;
                    case 'A' :
                        return currentHours > 11 ? meridiem.PM : meridiem.AM;
                    // 24 HOUR
                    case 'H' :
                        return currentHours;
                    case 'HH' :
                        return leftZeroFill(currentHours, 2);
                    // 12 HOUR
                    case 'h' :
                        return currentHours % 12 || 12;
                    case 'hh' :
                        return leftZeroFill(currentHours % 12 || 12, 2);
                    // MINUTE
                    case 'm' :
                        return currentMinutes;
                    case 'mm' :
                        return leftZeroFill(currentMinutes, 2);
                    // SECOND
                    case 's' :
                        return currentSeconds;
                    case 'ss' :
                        return leftZeroFill(currentSeconds, 2);
                    // TIMEZONE
                    case 'zz' :
                    // depreciating 'zz' fall through to 'z'
                    case 'z' :
                        return (m._d.toString().match(timezoneRegex) || [''])[0].replace(nonuppercaseLetters, '');
                    case 'Z' :
                        return (currentZone < 0 ? '-' : '+') + leftZeroFill(~~(Math.abs(currentZone) / 60), 2) + ':' + leftZeroFill(~~(Math.abs(currentZone) % 60), 2);
                    case 'ZZ' :
                        return (currentZone < 0 ? '-' : '+') + leftZeroFill(~~(10 * Math.abs(currentZone) / 6), 4);
                    // LONG DATES
                    case 'L' :
                    case 'LL' :
                    case 'LLL' :
                    case 'LLLL' :
                    case 'LT' :
                        return formatMoment(m, moment.longDateFormat[input]);
                    // DEFAULT
                    default :
                        return input.replace(/(^\[)|(\\)|\]$/g, "");
                }
            }
            return inputString.replace(charactersToReplace, replaceFunction);
        }

        // date from string and format string
        function makeDateFromStringAndFormat(string, format) {
            var inArray = [0, 0, 1, 0, 0, 0, 0],
                timezoneHours = 0,
                timezoneMinutes = 0,
                isUsingUTC = false,
                inputParts = string.match(inputCharacters),
                formatParts = format.match(tokenCharacters),
                len = Math.min(inputParts.length, formatParts.length),
                i,
                isPm;

            // function to convert string input to date
            function addTime(format, input) {
                var a;
                switch (format) {
                    // MONTH
                    case 'M' :
                    // fall through to MM
                    case 'MM' :
                        inArray[1] = ~~input - 1;
                        break;
                    case 'MMM' :
                    // fall through to MMMM
                    case 'MMMM' :
                        for (a = 0; a < 12; a++) {
                            if (moment.monthsParse[a].test(input)) {
                                inArray[1] = a;
                                break;
                            }
                        }
                        break;
                    // DAY OF MONTH
                    case 'D' :
                    // fall through to DDDD
                    case 'DD' :
                    // fall through to DDDD
                    case 'DDD' :
                    // fall through to DDDD
                    case 'DDDD' :
                        inArray[2] = ~~input;
                        break;
                    // YEAR
                    case 'YY' :
                        input = ~~input;
                        inArray[0] = input + (input > 70 ? 1900 : 2000);
                        break;
                    case 'YYYY' :
                        inArray[0] = ~~Math.abs(input);
                        break;
                    // AM / PM
                    case 'a' :
                    // fall through to A
                    case 'A' :
                        isPm = (input.toLowerCase() === 'pm');
                        break;
                    // 24 HOUR
                    case 'H' :
                    // fall through to hh
                    case 'HH' :
                    // fall through to hh
                    case 'h' :
                    // fall through to hh
                    case 'hh' :
                        inArray[3] = ~~input;
                        break;
                    // MINUTE
                    case 'm' :
                    // fall through to mm
                    case 'mm' :
                        inArray[4] = ~~input;
                        break;
                    // SECOND
                    case 's' :
                    // fall through to ss
                    case 'ss' :
                        inArray[5] = ~~input;
                        break;
                    // TIMEZONE
                    case 'Z' :
                    // fall through to ZZ
                    case 'ZZ' :
                        isUsingUTC = true;
                        a = (input || '').match(timezoneParseRegex);
                        if (a && a[1]) {
                            timezoneHours = ~~a[1];
                        }
                        if (a && a[2]) {
                            timezoneMinutes = ~~a[2];
                        }
                        // reverse offsets
                        if (a && a[0] === '+') {
                            timezoneHours = -timezoneHours;
                            timezoneMinutes = -timezoneMinutes;
                        }
                        break;
                }
            }
            for (i = 0; i < len; i++) {
                addTime(formatParts[i], inputParts[i]);
            }
            // handle am pm
            if (isPm && inArray[3] < 12) {
                inArray[3] += 12;
            }
            // if is 12 am, change hours to 0
            if (isPm === false && inArray[3] === 12) {
                inArray[3] = 0;
            }
            // handle timezone
            inArray[3] += timezoneHours;
            inArray[4] += timezoneMinutes;
            // return
            return isUsingUTC ? new Date(Date.UTC.apply({}, inArray)) : dateFromArray(inArray);
        }

        // compare two arrays, return the number of differences
        function compareArrays(array1, array2) {
            var len = Math.min(array1.length, array2.length),
                lengthDiff = Math.abs(array1.length - array2.length),
                diffs = 0,
                i;
            for (i = 0; i < len; i++) {
                if (~~array1[i] !== ~~array2[i]) {
                    diffs++;
                }
            }
            return diffs + lengthDiff;
        }

        // date from string and array of format strings
        function makeDateFromStringAndArray(string, formats) {
            var output,
                inputParts = string.match(inputCharacters),
                scores = [],
                scoreToBeat = 99,
                i,
                curDate,
                curScore;
            for (i = 0; i < formats.length; i++) {
                curDate = makeDateFromStringAndFormat(string, formats[i]);
                curScore = compareArrays(inputParts, formatMoment(new Moment(curDate), formats[i]).match(inputCharacters));
                if (curScore < scoreToBeat) {
                    scoreToBeat = curScore;
                    output = curDate;
                }
            }
            return output;
        }

        // date from iso format
        function makeDateFromString(string) {
            var format = 'YYYY-MM-DDT',
                i;
            if (isoRegex.exec(string)) {
                for (i = 0; i < 3; i++) {
                    if (isoTimes[i][1].exec(string)) {
                        format += isoTimes[i][0];
                        break;
                    }
                }
                return makeDateFromStringAndFormat(string, format + 'Z');
            }
            return new Date(string);
        }

        // helper function for _date.from() and _date.fromNow()
        function substituteTimeAgo(string, number, withoutSuffix) {
            var rt = moment.relativeTime[string];
            return (typeof rt === 'function') ?
                rt(number || 1, !!withoutSuffix, string) :
                rt.replace(/%d/i, number || 1);
        }

        function relativeTime(milliseconds, withoutSuffix) {
            var seconds = round(Math.abs(milliseconds) / 1000),
                minutes = round(seconds / 60),
                hours = round(minutes / 60),
                days = round(hours / 24),
                years = round(days / 365),
                args = seconds < 45 && ['s', seconds] ||
                    minutes === 1 && ['m'] ||
                    minutes < 45 && ['mm', minutes] ||
                    hours === 1 && ['h'] ||
                    hours < 22 && ['hh', hours] ||
                    days === 1 && ['d'] ||
                    days <= 25 && ['dd', days] ||
                    days <= 45 && ['M'] ||
                    days < 345 && ['MM', round(days / 30)] ||
                    years === 1 && ['y'] || ['yy', years];
            args[2] = withoutSuffix;
            return substituteTimeAgo.apply({}, args);
        }

        moment = function (input, format) {
            if (input === null || input === '') {
                return null;
            }
            var date,
                matched;
            // parse Moment object
            if (input && input._d instanceof Date) {
                date = new Date(+input._d);
                // parse string and format
            } else if (format) {
                if (isArray(format)) {
                    date = makeDateFromStringAndArray(input, format);
                } else {
                    date = makeDateFromStringAndFormat(input, format);
                }
                // evaluate it as a JSON-encoded date
            } else {
                matched = jsonRegex.exec(input);
                date = input === undefined ? new Date() :
                    matched ? new Date(+matched[1]) :
                            input instanceof Date ? input :
                        isArray(input) ? dateFromArray(input) :
                                typeof input === 'string' ? makeDateFromString(input) :
                            new Date(input);
            }
            return new Moment(date);
        };

        // creating with utc
        moment.utc = function (input, format) {
            if (isArray(input)) {
                return new Moment(new Date(Date.UTC.apply({}, input)), true);
            }
            return (format && input) ? moment(input + ' 0', format + ' Z').utc() : moment(input).utc();
        };

        // humanizeDuration
        moment.humanizeDuration = function (num, type, withSuffix) {
            var difference = +num,
                rel = moment.relativeTime,
                output;
            switch (type) {
                case "seconds" :
                    difference *= 1000; // 1000
                    break;
                case "minutes" :
                    difference *= 60000; // 60 * 1000
                    break;
                case "hours" :
                    difference *= 3600000; // 60 * 60 * 1000
                    break;
                case "days" :
                    difference *= 86400000; // 24 * 60 * 60 * 1000
                    break;
                case "weeks" :
                    difference *= 604800000; // 7 * 24 * 60 * 60 * 1000
                    break;
                case "months" :
                    difference *= 2592000000; // 30 * 24 * 60 * 60 * 1000
                    break;
                case "years" :
                    difference *= 31536000000; // 365 * 24 * 60 * 60 * 1000
                    break;
                default :
                    withSuffix = !!type;
                    break;
            }
            output = relativeTime(difference, !withSuffix);
            return withSuffix ? (difference <= 0 ? rel.past : rel.future).replace(/%s/i, output) : output;
        };

        // version number
        moment.version = VERSION;

        // default format
        moment.defaultFormat = isoFormat;

        // language switching and caching
        moment.lang = function (key, values) {
            var i,
                param,
                req,
                parse = [];
            if (values) {
                for (i = 0; i < 12; i++) {
                    parse[i] = new RegExp('^' + values.months[i] + '|^' + values.monthsShort[i].replace('.', ''), 'i');
                }
                values.monthsParse = values.monthsParse || parse;
                languages[key] = values;
            }
            if (languages[key]) {
                for (i = 0; i < paramsToParse.length; i++) {
                    param = paramsToParse[i];
                    moment[param] = languages[key][param] || moment[param];
                }
            } else {
                if (hasModule) {
                    req = require('./lang/' + key);
                    moment.lang(key, req);
                }
            }
        };

        // set default language
        moment.lang('en', {
            months : "一月_二月_三月_四月_五月_六月_七月_八月_九月_十月_十一月_十二月".split("_"),
            monthsShort : "一月_二月_三月_四月_五月_六月_七月_八月_九月_十月_十一月_十二月".split("_"),
            weekdays : "日_一_二_三_四_五_六".split("_"),
            weekdaysShort : "日_一_二_三_四_五_六".split("_"),
            longDateFormat : {
                LT : "h:mm A",
                L : "MM/DD/YYYY",
                LL : "MMMM D YYYY",
                LLL : "MMMM D YYYY LT",
                LLLL : "dddd, MMMM D YYYY LT"
            },
            meridiem : {
                AM : 'AM',
                am : 'am',
                PM : 'PM',
                pm : 'pm'
            },
            calendar : {
                sameDay : '[Today at] LT',
                nextDay : '[Tomorrow at] LT',
                nextWeek : 'dddd [at] LT',
                lastDay : '[Yesterday at] LT',
                lastWeek : '[last] dddd [at] LT',
                sameElse : 'L'
            },
            relativeTime : {
                future : "in %s",
                past : "%s ago",
                s : "a few seconds",
                m : "a minute",
                mm : "%d minutes",
                h : "an hour",
                hh : "%d hours",
                d : "a day",
                dd : "%d days",
                M : "a month",
                MM : "%d months",
                y : "a year",
                yy : "%d years"
            },
            ordinal : function (number) {
                var b = number % 10;
                return (~~ (number % 100 / 10) === 1) ? 'th' :
                    (b === 1) ? 'st' :
                        (b === 2) ? 'nd' :
                            (b === 3) ? 'rd' : 'th';
            }
        });

        // compare moment object
        moment.isMoment = function (obj) {
            return obj instanceof Moment;
        };

        // shortcut for prototype
        moment.fn = Moment.prototype = {

            clone : function () {
                return moment(this);
            },

            valueOf : function () {
                return +this._d;
            },

            'native' : function () {
                return this._d;
            },

            toString : function () {
                return this._d.toString();
            },

            toDate : function () {
                return this._d;
            },

            utc : function () {
                this._isUTC = true;
                return this;
            },

            local : function () {
                this._isUTC = false;
                return this;
            },

            format : function (inputString) {
                return formatMoment(this, inputString ? inputString : moment.defaultFormat);
            },

            add : function (input, val) {
                this._d = dateAddRemove(this._d, input, 1, val);
                return this;
            },

            subtract : function (input, val) {
                this._d = dateAddRemove(this._d, input, -1, val);
                return this;
            },

            diff : function (input, val, asFloat) {
                var inputMoment = moment(input),
                    zoneDiff = (this.zone() - inputMoment.zone()) * 6e4,
                    diff = this._d - inputMoment._d - zoneDiff,
                    year = this.year() - inputMoment.year(),
                    month = this.month() - inputMoment.month(),
                    date = this.date() - inputMoment.date(),
                    output;
                if (val === 'months') {
                    output = year * 12 + month + date / 30;
                } else if (val === 'years') {
                    output = year + month / 12;
                } else {
                    output = val === 'seconds' ? diff / 1e3 : // 1000
                            val === 'minutes' ? diff / 6e4 : // 1000 * 60
                            val === 'hours' ? diff / 36e5 : // 1000 * 60 * 60
                            val === 'days' ? diff / 864e5 : // 1000 * 60 * 60 * 24
                            val === 'weeks' ? diff / 6048e5 : // 1000 * 60 * 60 * 24 * 7
                        diff;
                }
                return asFloat ? output : round(output);
            },

            from : function (time, withoutSuffix) {
                return moment.humanizeDuration(this.diff(time), !withoutSuffix);
            },

            fromNow : function (withoutSuffix) {
                return this.from(moment(), withoutSuffix);
            },

            calendar : function () {
                var diff = this.diff(moment().sod(), 'days', true),
                    calendar = moment.calendar,
                    allElse = calendar.sameElse,
                    format = diff < -6 ? allElse :
                            diff < -1 ? calendar.lastWeek :
                            diff < 0 ? calendar.lastDay :
                            diff < 1 ? calendar.sameDay :
                            diff < 2 ? calendar.nextDay :
                            diff < 7 ? calendar.nextWeek : allElse;
                return this.format(typeof format === 'function' ? format.apply(this) : format);
            },

            isLeapYear : function () {
                var year = this.year();
                return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
            },

            isDST : function () {
                return (this.zone() < moment([this.year()]).zone() ||
                    this.zone() < moment([this.year(), 5]).zone());
            },

            day : function (input) {
                var day = this._d.getDay();
                return (typeof input === 'undefined') ? day :
                    this.add({ d : input - day });
            },

            sod: function () {
                return this.clone()
                    .hours(0)
                    .minutes(0)
                    .seconds(0)
                    .milliseconds(0);
            },

            eod: function () {
                // end of day = start of day plus 1 day, minus 1 millisecond
                return this.sod().add({
                    d : 1,
                    ms : -1
                });
            },

            zone : function () {
                return this._isUTC ? 0 : this._d.getTimezoneOffset();
            },

            daysInMonth : function () {
                return this.clone().month(this.month() + 1).date(0).date();
            }
        };

        // helper for adding shortcuts
        function makeShortcut(name, key) {
            moment.fn[name] = function (input) {
                var utc = this._isUTC ? 'UTC' : '';
                if (typeof input !== 'undefined') {
                    this._d['set' + utc + key](input);
                    return this;
                } else {
                    return this._d['get' + utc + key]();
                }
            };
        }

        // loop through and add shortcuts (Month, Date, Hours, Minutes, Seconds, Milliseconds)
        for (i = 0; i < shortcuts.length; i ++) {
            makeShortcut(shortcuts[i].toLowerCase(), shortcuts[i]);
        }

        // add shortcut for year (uses different syntax than the getter/setter 'year' == 'FullYear')
        makeShortcut('year', 'FullYear');

        return moment;
    })(Date);

//function to reset the date object to 00:00 GMT
    moment.fn.stripTime = function () {
        this._d = new Date(Math.floor(this._d.valueOf() / 86400000) * 86400000);
        return this;
    }


//function to get the total number of days since the epoch.
    moment.fn.yearDay = function (input) {
        var yearday = Math.floor(this._d / 86400000);
        return (typeof input === 'undefined') ? yearday :
            this.add({ d : input - yearday });
    }

    today = moment().stripTime();

    if (typeof jQuery !== 'undefined') {
        jQuery.fn.kalendae = function (options) {
            this.each(function (i, e) {
                if (e.tagName === 'INPUT') {
                    //if element is an input, bind a popup calendar to the input.
                    $(e).data('kalendae', new Kalendae.Input(e, options));
                } else {
                    //otherwise, insert a flat calendar into the element.
                    $(e).data('kalendae', new Kalendae($.extend({}, {attachTo:e}, options)));
                }
            });
            return this;
        }
    }


})();
